using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace WebServer {
    public class MyWebServer {
        private readonly EventArgs _ea = new EventArgs();
        private readonly Thread _th;
        public EventHandler Messages;
        private FileStream _fs;
        private int _iStartPos;
        public bool IsRuning;
        private TcpListener _myListener;
        private Socket _mySocket;
        private int port = 5050; // Select any free port you wish
        private String _sDirName;
        private String _sErrorMessage;
        private String sFormattedMessage = "";
        private String _sLocalDir;
        private String _sMyWebServerRoot;
        private String _sPhysicalFilePath = "";
        private String _sRequest;
        private String _sRequestedFile;
        private String _sResponse = "";

        public MyWebServer(string root)
            : this() {
            _sMyWebServerRoot = root;
        }

        public MyWebServer() {
            _th = new Thread(StartListen);
        }

        public void Dummy(object mess, EventArgs e) {
        }

        //The constructor which make the TcpListener start listening on the
        //given port. It also calls a Thread on the method StartListen(). 
        public void Start() {
            try {
                //start listing on the given port
                Messages += Dummy;
                _myListener = new TcpListener(IPAddress.Any, port);
                _myListener.Start();
                //start the thread which calls the method 'StartListen'               
                _th.Start();
                IsRuning = true;
            } catch (Exception ex) {
                Messages.Invoke("An Exception Occurred while Listening :" + ex.Message, _ea);
            }
        }

        /// <summary>
        /// Returns The Default File Name
        /// Input : WebServerRoot Folder
        /// Output: Default File Name
        /// </summary>
        /// <param name="sLocalDirectory"></param>
        /// <returns></returns>
        public string GetTheDefaultFileName(string sLocalDirectory) {
            String sLine = "";
            try
            {
                //Open the default.dat to find out the list
                // of default file
                StreamReader sr = new StreamReader("data\\Default.Dat");

                while ((sLine = sr.ReadLine()) != null) {
                    //Look for the default file in the web server root folder
                    if (File.Exists(sLocalDirectory + sLine))
                        break;
                }
            }
            catch (Exception ex) {
                Messages.Invoke("An Exception Occurred : " + ex.Message, _ea);
            }
            if (File.Exists(sLocalDirectory + sLine))
                return sLine;
            return "";
        }


        /// <summary>
        /// This function takes FileName as Input and returns the mime type..
        /// </summary>
        /// <param name="sRequestedFile">To indentify the Mime Type</param>
        /// <returns>Mime Type</returns>
        public string GetMimeType(string sRequestedFile) {
            String sMimeType = "";
            String sFileExt;
            String sMimeExt = "";

            // Convert to lowercase
            sRequestedFile = sRequestedFile.ToLower();

            int iStartPos = sRequestedFile.IndexOf(".");

            sFileExt = sRequestedFile.Substring(iStartPos);

            try
            {
                //Open the Vdirs.dat to find out the list virtual directories
                StreamReader sr = new StreamReader(_sMyWebServerRoot + "data\\Mime.Dat");

                String sLine = "";
                while ((sLine = sr.ReadLine()) != null) {
                    sLine.Trim();

                    if (sLine.Length > 0) {
                        //find the separator
                        iStartPos = sLine.IndexOf(";");

                        // Convert to lower case
                        sLine = sLine.ToLower();

                        sMimeExt = sLine.Substring(0, iStartPos);
                        sMimeType = sLine.Substring(iStartPos + 1);

                        if (sMimeExt == sFileExt)
                            break;
                    }
                }
            }
            catch (Exception ex) {
                Messages.Invoke("An Exception Occurred : " + ex.Message, _ea);
            }

            if (sMimeExt == sFileExt)
                return sMimeType;
            return "";
        }


        /// <summary>
        /// Returns the Physical Path
        /// </summary>
        /// <param name="sMyWebServerRoot">Web Server Root Directory</param>
        /// <param name="sDirName">Virtual Directory </param>
        /// <returns>Physical local Path</returns>
        public string GetLocalPath(string sMyWebServerRoot, string sDirName) {
            StreamReader sr;
            String sLine = "";
            String sVirtualDir = "";
            String sRealDir = "";
            int iStartPos = 0;
            //Remove extra spaces
            sDirName.Trim();
            // Convert to lowercase
            sMyWebServerRoot = sMyWebServerRoot.ToLower();
            // Convert to lowercase
            sDirName = sDirName.ToLower();
            //Remove the slash
            //sDirName = sDirName.Substring(1, sDirName.Length - 2);
            try {
                //Open the Vdirs.dat to find out the list virtual directories
                sr = new StreamReader(sMyWebServerRoot + "data\\VDirs.Dat");

                while ((sLine = sr.ReadLine()) != null) {
                    //Remove extra Spaces
                    sLine.Trim();

                    if (sLine.Length > 0) {
                        //find the separator
                        iStartPos = sLine.IndexOf(";");

                        // Convert to lowercase
                        sLine = sLine.ToLower();

                        sVirtualDir = sLine.Substring(0, iStartPos);
                        sRealDir = sLine.Substring(iStartPos + 1);

                        if (sVirtualDir == sDirName) {
                            break;
                        }
                    }
                }
            } catch (Exception ex) {
                Messages.Invoke("An Exception Occurred : " + ex.Message, _ea);
            }


            Messages.Invoke("Virtual Dir : " + sVirtualDir, _ea);
            Messages.Invoke("Directory   : " + sDirName, _ea);
            Messages.Invoke("Physical Dir: " + sRealDir, _ea);
            if (sVirtualDir == sDirName)
                return sRealDir;
            return "";
        }


        /// <summary>
        /// This function send the Header Information to the client (Browser)
        /// </summary>
        /// <param name="sHttpVersion">HTTP Version</param>
        /// <param name="sMIMEHeader">Mime Type</param>
        /// <param name="iTotBytes">Total Bytes to be sent in the body</param>
        /// <param name="mySocket">Socket reference</param>
        /// <returns></returns>
        public void SendHeader(string sHttpVersion, string sMIMEHeader, int iTotBytes, string sStatusCode,
                               ref Socket mySocket) {
            String sBuffer = "";

            // if Mime type is not provided set default to text/html
            if (sMIMEHeader.Length == 0) {
                sMIMEHeader = "text/html"; // Default Mime Type is text/html
            }

            sBuffer = sBuffer + sHttpVersion + sStatusCode + "\r\n";
            sBuffer = sBuffer + "Server: cx1193719-b\r\n";
            sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n";
            sBuffer = sBuffer + "Accept-Ranges: bytes\r\n";
            sBuffer = sBuffer + "Content-Length: " + iTotBytes + "\r\n\r\n";

            Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer);

            SendToBrowser(bSendData, ref mySocket);

            Messages.Invoke("Total Bytes : " + iTotBytes.ToString(), _ea);
        }


        /// <summary>
        /// Overloaded Function, takes string, convert to bytes and calls 
        /// overloaded sendToBrowserFunction.
        /// </summary>
        /// <param name="sData">The data to be sent to the browser(client)</param>
        /// <param name="mySocket">Socket reference</param>
        public void SendToBrowser(String sData, ref Socket mySocket) {
            SendToBrowser(Encoding.ASCII.GetBytes(sData), ref mySocket);
        }


        /// <summary>
        /// Sends data to the browser (client)
        /// </summary>
        /// <param name="bSendData">Byte Array</param>
        /// <param name="mySocket">Socket reference</param>
        public void SendToBrowser(Byte[] bSendData, ref Socket mySocket) {
            int numBytes = 0;

            try {
                if (mySocket.Connected) {
                    if ((numBytes = mySocket.Send(bSendData, bSendData.Length, 0)) == -1)
                        Messages.Invoke("Socket Error cannot Send Packet", _ea);
                    else {
                        Messages.Invoke("No. of bytes send" + numBytes, _ea);
                    }
                } else
                    Messages.Invoke("Connection Dropped....", _ea);
            } catch (Exception ex) {
                Messages.Invoke("Error Occurred : " + ex.Message, _ea);
            }
        }


        // Application Starts Here..d
        //public static void Main() {
        //    MyWebServer MWS = new MyWebServer();
        //}

        public void Stop() {
            IsRuning = false;
            try {
                _th.Join();
                _myListener.Stop();
                _mySocket.Dispose();
            } catch (ThreadStateException) {
            } catch (Exception)
            {
            }
        }

        //This method Accepts new connection and
        //First it receives the welcome massage from the client,
        //Then it sends the Current date time to the Client.

        public void StartListen() {
            _sMyWebServerRoot = (_sMyWebServerRoot.Equals("") ? @"\Http\" : _sMyWebServerRoot);
            while (true) {
                //Accept a new connection
                if (_myListener.Pending()) {
                    #region if ...

                    _mySocket = _myListener.AcceptSocket();

                    Messages.Invoke("Socket Type " + _mySocket.SocketType, _ea);
                    if (_mySocket.Connected) {
                        Messages.Invoke("\nClient Connected!!\n==================\nCLient IP" +
                                        _mySocket.RemoteEndPoint + " \n", _ea);
                        //make a byte array and receive data from the client 
                        var bReceive = new Byte[1024];
                        int i = _mySocket.Receive(bReceive, bReceive.Length, 0);
                        //Convert Byte to String
                        string sBuffer = Encoding.ASCII.GetString(bReceive);
                        //At present we will only deal with GET type
                        if (sBuffer.Substring(0, 3) != "GET") {
                            Messages.Invoke("Only Get Method is supported..", _ea);
                            _mySocket.Close();
                            return;
                        }
                        // Look for HTTP request
                        _iStartPos = sBuffer.IndexOf("HTTP", 1);
                        // Get the HTTP text and version e.g. it will return "HTTP/1.1"
                        string sHttpVersion = sBuffer.Substring(_iStartPos, 8);
                        // Extract the Requested Type and Requested file/directory
                        _sRequest = sBuffer.Substring(0, _iStartPos - 1);
                        //Replace backslash with Forward Slash, if Any
                        _sRequest.Replace("\\", "/");
                        //If file name is not supplied add forward slash to indicate 
                        //that it is a directory and then we will look for the 
                        //default file name..
                        if ((_sRequest.IndexOf(".") < 1) && (!_sRequest.EndsWith("/"))) {
                            _sRequest = _sRequest + "/";
                        }


                        //Extract the requested file name
                        _iStartPos = _sRequest.LastIndexOf("/") + 1;
                        _sRequestedFile = _sRequest.Substring(_iStartPos);
                        //Extract The directory Name
                        _sDirName = _sRequest.Substring(_sRequest.IndexOf("/"), _sRequest.LastIndexOf("/") - 3);
                        /////////////////////////////////////////////////////////////////////
                        // Identify the Physical Directory
                        /////////////////////////////////////////////////////////////////////
                        if (_sDirName == "/")
                            _sLocalDir = _sMyWebServerRoot;
                        else {
                            //Get the Virtual Directory
                            _sLocalDir = GetLocalPath(_sMyWebServerRoot, _sDirName);
                        }
                        Messages.Invoke("Directory Requested : " + _sLocalDir, _ea);
                        //If the physical directory does not exists then
                        // dispaly the error message
                        if (_sLocalDir.Length == 0) {
                            _sErrorMessage = "<H2>Error!! Requested Directory does not exists</H2><Br>";
                            _sErrorMessage = _sErrorMessage + "Please check data\\Vdirs.Dat";
                            //Format The Message
                            SendHeader(sHttpVersion, "", _sErrorMessage.Length, " 404 Not Found", ref _mySocket);
                            //Send to the browser
                            SendToBrowser(_sErrorMessage, ref _mySocket);
                            _mySocket.Close();
                            continue;
                        }

                        /////////////////////////////////////////////////////////////////////
                        // Identify the File Name
                        /////////////////////////////////////////////////////////////////////

                        //If The file name is not supplied then look in the default file list
                        if (_sRequestedFile.Length == 0) {
                            // Get the default filename
                            _sRequestedFile = GetTheDefaultFileName(_sLocalDir);
                            if (_sRequestedFile == "") {
                                _sErrorMessage = "<H2>Error!! No Default File Name Specified</H2>";
                                SendHeader(sHttpVersion, "", _sErrorMessage.Length, " 404 Not Found", ref _mySocket);
                                SendToBrowser(_sErrorMessage, ref _mySocket);
                                _mySocket.Close();
                                return;
                            }
                        }
                        /////////////////////////////////////////////////////////////////////
                        // Get TheMime Type
                        /////////////////////////////////////////////////////////////////////

                        String sMimeType = GetMimeType(_sRequestedFile);
                        //Build the physical path
                        _sPhysicalFilePath = _sLocalDir + _sRequestedFile;
                        Messages.Invoke("File Requested : " + _sPhysicalFilePath, _ea);
                        if (File.Exists(_sPhysicalFilePath) == false) {
                            _sErrorMessage = "<H2>404 Error! File Does Not Exists...</H2>";
                            SendHeader(sHttpVersion, "", _sErrorMessage.Length, " 404 Not Found", ref _mySocket);
                            SendToBrowser(_sErrorMessage, ref _mySocket);
                            Messages.Invoke(sFormattedMessage, _ea);
                        } else {
                            int iTotBytes = 0;
                            _sResponse = "";
                        //check if its awailable
                        BACK:
                            try {
                                _fs = new FileStream(_sPhysicalFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
                            } catch (IOException) {
                                //the file is unavailable because it is:
                                //still being written to
                                //or being processed by another thread
                                //or does not exist (has already been processed)
                                Thread.Sleep(1);
                                goto BACK;
                            }

                            // Create a reader that can read bytes from the FileStream.
                            var reader = new BinaryReader(_fs);
                            var bytes = new byte[_fs.Length];
                            int read;
                            while ((read = reader.Read(bytes, 0, bytes.Length)) != 0) {
                                // Read from the file and write the data to the network
                                _sResponse = _sResponse + Encoding.ASCII.GetString(bytes, 0, read);
                                iTotBytes = iTotBytes + read;
                            }
                            reader.Close();
                            _fs.Close();
                            SendHeader(sHttpVersion, sMimeType, iTotBytes, " 200 OK", ref _mySocket);
                            SendToBrowser(bytes, ref _mySocket);
                            //mySocket.Send(bytes, bytes.Length,0);
                        }
                        _mySocket.Close();
                    }

                    #endregion
                } else {
                    if (!IsRuning) return;
                    Thread.Sleep(50);
                }
            }
        }
    }
}